You may recall the discussion near the beginning of Chapter inventor.htm of native vs. user defined variable types. I provided a list of native C++ variable types; namely, char, short, long, float, double, bool, and int. We've already created several classes for our inventory control project, and now it's time to apply what we've learned to a more basic type, the string. We've been using strings for a long time, and now it's time to see exactly how to implement the string class we've been using.
By the end of this chapter, you should
string class are created, destroyed, and assigned to one another.
string class.
stringSusan had some questions about these objectives. Here's the discussion.
Susan: What is the difference between literal C strings and variables of theBefore we get into how to create thestringclass?
Steve: A variable of the
stringclassis what you've been using to store variable-length alphanumeric data. You can copy them, input them from the keyboard, assign values to them, and the like. By contrast, a literal C string is just a bunch of characters in a row; all you can do with it is display it or assign it to astringvariable.
Susan: OK, then you are saying that variables of the
stringclassare what I am used to working with, but I just didn't know that they were part of aclassbecause you were keeping this all a big secret. On the other hand, a literal C string is just some nonsense that you want me to learn to assign to something that might make sense? OK, this is great; sure, this is logical<?>. Hey, a literal C string must be a part of the native language?
Steve: Right all the way along.
Susan: Yes, but why would something so basic as
stringnot be part of the native language? This is what I don't understand. Andvectors too; even though they are gross, I can see that they are a very necessary evil. So tell me why those basic things would not be part of the native language?
Steve: That's a very good question. In fact, both
strings andvectors are going to be supplied by the "standard library" that will be part of the C++ standard, which should take effect about the time this book comes out (mid-1996). They still won't be part of the language itself, because that would make the compiler even more complicated than it already is. However, the user will still be able to use them without having to make them up, so the fact that they aren't part of the language will be of mostly theoretical interest.
string class we've been using in this book, I should expand on the answer I gave Susan as to why string isn't a native type in the first place: To keep the C++ language itself as simple as possible. One of the design goals of C++, as of C, was to allow the language to be moved, or ported, from one machine type to another as easily as possible. Since strings, vectors, and so on can be written in C++ (i.e., created out of the more elementary parts of the language), they don't have to be built in. This reduces the amount of effort needed to port C++. In addition, some applications don't need and can't afford anything but the barest essentials; "embedded" CPUs such as those in cameras, VCRs, elevators, or microwave ovens, are probably the most important users of such applications, and such devices are much more common than "real" computers. However, this still leaves the question of why these data types haven't been in a standard library that can be used when necessary. Bjarne says in his book Design and Evolution of C++ that it might very well have been a mistake to have released C++ to the general public without this library; I tend to agree. In any event, this situation will be remedied when the C++ standard is adopted, which as mentioned is scheduled to occur sometime next year (1998).
Even though strings aren't native, we've been using them for some time already without realizing that they're not native variables, so it should be fairly obvious that this class has to provide the facilities of a concrete data type; that is, one whose objects can be created, copied, assigned, and destroyed as though they were native variables. You may recall from the discussion starting on page in Chapter inventor.htm that such data types need a default constructor, a copy constructor, an assignment operator, and a destructor. To refresh your memory, here's the description of each of these member functions:
In our StockItem and Inventory class definitions, the compiler-generated versions of these functions were fine for all but the default constructor. In the case of the string class, though, we're going to have to create our own versions of all four of these functions, for reasons that will become apparent as we examine their implementations in this chapter and the next one. Before we can implement these member functions for our string class, though, we have to define exactly what a string is. The string class is a data type that gives us the following capabilities in addition to those facilities that every concrete data type provides:
string to a literal value like "abc".
string on the screen with the << operator.
string in from the keyboard with the >> operator.
strings to find out whether they are equal.
strings to find out which is "less than" the other; that is, which one would come first in the dictionary.
We'll see how all of these capabilities work sometime in this chapter or the next one, but for now let's start with Figure stringinterface1, a simplified version of the interface specification for the string class, which includes the specification of the four member functions needed for a concrete data type, as well as a special constructor that is specific to the string class. I strongly recommend that you print out the files that contain this interface and its implementation, as well as the test program, for reference as you are going through this part of the chapter; those files are string1.h, string1.cc, and strtst1.cc, respectively.
string class interface, initial version (code\string1.h)
(Figure stringinterface1)code/string1.h
The first four member functions in that interface are the standard concrete data type functions. In order, they are
operator =
I've been instructed by Susan to let you see all of the code that implements this initial version of our string class at once before we start to analyze it. Of course, I've done so, and Figure stringimpl is the result.
string class (code\string1.cc)
(Figure stringimpl)code/string1.cc
Now let's start by looking at the default constructor. Figure stringdefaultcons shows its implementation.
string class (from code\string1.cc)
(Figure stringdefaultcons)string::string(): m_Length(1),
m_Data(new char [m_Length])
{
memcpy(m_Data,"",m_Length);
}
The member initialization list in this constructor contains two expressions. The first of them, m_Length(1), isn't very complicated at all. It simply sets the length of our new string to 1. However, this may seem a bit odd: Why do we need any characters at all for a string that has no value? The answer to this riddle is quite simple: To make our strings as compatible as possible with pre-existing C functions that work on C strings, we need to include the null byte that terminates all C strings, so we need to reserve one more byte of memory for a string's data than are needed to hold the characters that we actually want to store. In the current case of a zero-character string, this means that we need one byte of storage for the null byte.
It's important to note that this is an example where the order of execution of member initializer expressions is important: we definitely want m_Length to be initialized before m_Data, because the amount of data being assigned to m_Data depends on the value of m_Length. As you may remember from Chapter inventor.htm, the order in which the initialization expressions are executed is dependent not on the order in which they are written in the list, but on the order in which the member variables being initialized are declared in the class interface definition. Therefore, it's important to make sure that the order in which those member variables are declared is correct. In this case, it is, because m_Length is declared before m_Data in string1.h.
Before proceeding to the next member initialization expression, let's take a look at the characteristics of the variables that we're using here. The scope of these variables, as we know from our previous discussion of the StockItem class, is class scope; therefore, each object of the string class has its own set of these variables, and they are accessible from any member functions of the class as though they were global variables.
However, an equally important characteristic of each of these variables is its data type. The type of m_Length is short, which is a type we've encountered before, a 16-bit integer variable that can hold a number between -32768 and 32767. But what about the type of the other member variable, m_Data, which is listed in Figure stringinterface1 as char*? We know what a char is, but what does that * mean?
The star means pointer, which is just another term for a memory address. In particular, char* (pronounced "char star") means "pointer to a char".1 This is considered one of the most difficult concepts for beginning programmers to grasp, but you shouldn't have any trouble understanding its definition if you've been following the discussion so far: A pointer is the address of some data item in memory. That is, to say "a variable points to a memory location" is almost exactly the same as saying "a variable's value is the address of a memory location". In the specific case of a variable x of type char*, for example, to say "x points to a C string" is exactly the same as saying "x contains the address of the first byte of the C string."2 The m_Data variable is used to hold the address of the first char of the data that a string contains; the rest of the characters follow the first character at consecutively higher locations in memory.
If this sounds familiar, it should; a literal C string like "hello" (Chapter basics.htm) consists of a number of chars in consecutive memory locations; it should come as no surprise then when I tell you that a literal C string has the type char*.
As you might infer from these cases, our use of one char* to refer to multiple chars isn't an isolated example. Actually, it's quite a widespread practice in C++, which brings up an important point: A char*, or any other type of pointer for that matter, has two different possible meanings in C++.3 One of these meanings is the obvious one of signifying the address of a single item of the type the pointer points to. In the case of a char*, that means the address of a char. However, in the case of a literal C string, as well in the case of our m_Data member variable, we use a char* to indicate the address of the first char of an indeterminate number of chars; any chars after the first one occupy consecutively higher addresses in memory. Most of the time, this distinction has little effect on the way we write programs, but sometimes we have to be sensitive to this "multiple personality" of pointers; we'll run across one of these cases later in this chapter.
Susan had some questions (and I had some answers) on this topic of the true meaning of a char*:
Susan: What I get from this is thatWhile we're on the subject of that earlier discussion of literal C strings, you may recall that I bemoaned the fact that such literal C strings use a 0 byte to mark the end of the literal value, rather than keeping track of the length separately. Nothing can be done about that decision now, at least as it applies to literal C strings. In the case of thechar*points to acharaddress either singularly or as the beginning of a string of multiple addresses. Is that right?
Steve: Yes, except that it's a string of several characters, not addresses.
Susan: Oh, here we go again; this is so confusing. So if I use a
string"my name is"thenchar*points to the address that holds thestringof all those letters. But if the number of letters exceeds what the address can hold, won't it take up the next available address in memory andchar*point to it after it points to the first address?
Steve: Each memory address can hold 1 byte; in the case of a
string, that byte is the value of onecharof thestring's data. So achar*, as we use it, will always point to the firstcharof thechars that hold ourstring's value; the otherchars follow that one immediately in memory.
Susan: Let me ask this: When you show an example of a
stringwith the value"Test"(Figure strnmem), the pointer at address12340002containing the address1234febcis really pointing at theTas that would be the firstcharand the rest of the letters will actually be in the other immediately following bytes of memory?
Steve: Absolutely correct.
string class, however, the implementation is under our control rather than the language designer's; therefore, I've decided to use a length variable (m_Length) along with the variable that holds the address of the first char of the data (m_Data).
In other words, what we're doing in this exercise is synthesizing a new data type called string. A string needs a length and a set of characters to represent the actual data in the string. The short named m_Length is used in the string class to keep track of the number of characters in the data part of the string; the char* named m_Data is used to hold the address of the first character of the data part of the string.
The next member initialization expression, m_Data(new char [m_Length]), takes us on another of our side trips. This one has to do with the dreaded topic of dynamic memory allocation.
new and deleteSo far, we've encountered two storage classes: static and auto. As you might recall from the discussion in Chapter function.htm, static variables are allocated memory when the program is linked, while the memory for auto variables is assigned to them at entry to the block where they are defined. However, both mechanisms have a major limitation: The amount of memory needed is fixed when the program is compiled. In the case of a string, we need to allocate an amount of memory that cannot be known until the program is executed, so we need another storage class.
As you will be happy to learn, there is indeed another storage class called dynamic storage that enables us to decide the amount of memory to allocate at run time.4 To allocate memory dynamically, we use the new operator, specifying the data type of the memory to be allocated and the number of elements that we need. In our example member initialization expression, m_Data(new char [m_Length]), the type is char and the count is m_Length. The result of calling new is a pointer to the specified data type; in this case, since we want to store chars, the result of calling new is a pointer to a char, that is, a char*. This is a good thing, because char* is the type of the variable m_Data that we're initializing to the address that is returned from new. So the result of the member initialization expression we're examining is to set m_Data to the value returned from calling new; that value is the address of a newly assigned block of memory that can hold m_Length chars. In the case of the default constructor, we've asked for a block of 1 byte, which is just what we need to hold the contents of the zero-length C string that represents the value of our empty string.
It may not be obvious why we need to call new to get the address where we will store our data. Doesn't a char* always point to a byte in memory? Yes, it does; the problem is which byte. We can't use static (link time) or auto (function entry time) allocation for our string class, because each string can have a different number of characters. Therefore, we have to assign the memory after we find out how many characters we need to store the value of the string. The new operator reserves some memory and returns the address of the beginning of that memory. In this case, we assign that address to our char* variable called m_Data. An important point to note here is that in addition to giving us the address of a section of memory, new also gives us the right to use that memory for our own purposes. That same memory area will not be made available for any other use until we say we're done with it by calling another operator called delete. Susan had some questions about how (and why) we use new. Here's the discussion:
Susan: OK, so all Figure stringdefaultcons does is lay the foundation to be able to acquire memory to store the C string "" and then copy that information that will go intom_Datathat starts at a certain location in memory?
Steve: Right; Figure stringcharstarcons is the code for the constructor that accomplishes that task.
Susan: When you say that "the amount of memory needed is fixed when the program is compiled" that bothers me. I don't understand that in terms of
autovariables, or is this because that type is known such as ashort?
Steve: Right. As long as the types and the quantity of the data items in a
classdefinition are known at compile time, as is the case withautoandstaticvariables, the compiler can figure out the amount of memory they need. The addresses ofautovariables aren't known at compile time, but how much space they will take up is.
Susan: OK, I understand the types of the data items. However, I am not sure what you mean by the quantity; can you give me an example?
Steve: Sure. You might have three
chars and fourshorts in a particularclassdefinition; in that case, the compiler would add up three times the length of acharand four times the length of ashortand allocate that much memory (more or less). Actually, some other considerations affect the size of aclassobject that aren't relevant to the discussion here, but they can all be handled at compile time and therefore still allow the compiler to figure out the amount of memory needed to store an object of anyclass.
The statement inside the {} in the default constructor, memcpy(m_Data,"",m_Length); is responsible for copying the null byte from the C string "" to our newly allocated area of memory. The function memcpy (short for "memory copy") is one of the C standard library functions for C string and memory manipulation. As you can see, it takes three arguments. The first argument is a pointer to the destination, that is, the address that will receive the data. The second argument is a pointer to the source of the data; this, of course, is the address that we're copying from (i.e., the address of the null byte in the "", in our example). The last argument is the number of bytes to copy.
In other words, memcpy reads the bytes that starts at the address specified by its input argument (in this case, "") and writes a copy of those bytes to addresses starting at the address specified by its output argument (in this case, m_Data). The amount copied is specified by the length argument (in this case, m_Length). Effectively, therefore, memcpy copies a certain amount of data from one place in memory to another; in this case, it copies 1 byte from the address of the literal C string "" to the address pointed to by m_Data (that is, the place where we're storing the data that make up the value of our string).
This notion of dynamic allocation was the subject of some more discussion with Susan.
Susan: This stuff with operatornew:I have no idea what you are talking about. I am totally gone, left in the dust. What is this stuff? Why do you neednewto point to memory locations, I thought that is whatchar*did?
Steve: You're right that
char*points to a memory location. But which one? The purpose ofnewis to get some memory for us from the operating system and return the address of the first byte of that memory. In this case, we assign that address to ourchar*variable calledm_Data. Afterward, we can store data at that address.
Susan: I am not getting this because I just don't get the purpose of
char*, and don't just tell me that it points to an address in memory. I want to know why we need it to point to a specific address in memory rather than let it take on just any random address in memory.
Steve: Because then there would be no way of guaranteeing that the memory that it points to won't be used by some other part of the program or indeed some other program entirely in a multitasking system like Windows. We need to claim ownership of some memory by calling
newbefore we can use it.
Susan: I think I understand now why we need to use
new, but why should the result of callingnewbe a pointer? I am missing this completely. How doesnewresult inchar*? Some steps are not obvious here.
Steve: Because that's how
newis defined: it gives you an address (pointer) to a place where you can store somechars (or whatever type you requested).
Susan: OK, but in the statement
m_Data = new char [m_Length], why ischarin this statement notchar*? I am getting so confused on this.
Steve: Because you're asking for an address (pointer) to a place where you can store a bunch of
chars.
Susan: But then wouldn't it be necessary to specify
char*rather thancharin the statement?
Steve: I admit that I find that syntax unclear as well. Yes, in my opinion, it should be stated as
char*, but apparently Bjarne thought otherwise.
Susan: OK, so then
m_Datais the pointer address wherenew(memory from the heap) is going to storechardata ofm_Length. Right?
Steve: Yes, the value assigned to
m_Datain the constructor is the value returned fromnew; this value is the address of an area of memory allocated to this use. The area of memory is of lengthm_Length.
Susan: Well, I thought that the address stored in
m_Datawas the first place where you stored yourchars. So isnewjust what goes and gets that memory to put the address inm_Data?
Steve: Exactly.
Susan: Here's what I understand about the purpose of
char*. It functions as a pointer to a specific memory address. We need to do that because the computer is stupid and doesn't know where to put thechardata, therefore we needchar*to say "hey you, computer, look over here, this is where we are going to put the data for you to find and use".
Steve: That's fine.
Susan: We need to use
char*for variable length memory. This is because we don't know how much memory we will need until it is used. For this we need the variablem_Datato hold the first address in memory for ourchardata. Then we need the variablem_Lengththat we have set to the length of the C string that will be used to get the initial data for thestring. Then we have to have that nifty little helper guynewto get some memory from the heap for the memory of our C string data.
Steve: Sounds good to me.
Susan: Now about
memcpy: This appears to be the same thing as initializing the variable. I am so confused.
Steve: That's exactly correct. Maybe you shouldn't get unconfused!
As the call to memcpy is the only statement in the constructor proper, it's time to see what we have accomplished. The constructor has initialized a string by
string to the effective length of a null C string, "", including the terminating null byte (i.e., 1 byte).
Now let's continue with our examination of the string::string() constructor. Its final result is a string with the value "", whose memory layout might look like Figure strempty.5
string in memory
(Figure strempty)
Address Name string n +--------------------+ 12340000 m_Length | 0001 | +--------------------+ 12340002 m_Data | 1234febc +-+ +--------------------+ | | +--------------------+ | 1234febc (none) | 00 |±+ +--------------------+
Using the default constructor is considerably easier than defining it. As we have seen in Chapter inventor.htm, the default constructor is called whenever we declare an object without specifying any data to initialize it with; for example, in the line string s; in Figure strtst1. Although this program doesn't do anything useful, it does illustrate how we can use the member functions of the string class, so you should pay attention to it.
string class (code\strtst1.cc)
(Figure strtst1)code/strtst1.cc
In case it's still not clear why the line string s; calls the default constructor string::string(), here's the detailed explanation:
string s; means that we want to create a variable of type string with the name s.
string is not a native data type, the compiler looks for a function called string::string, which would create a string. It finds one because we've included the file string1.h in our example program; the contents of that file are listed in Figure stringinterface1.
string::string, with different argument lists, because there are several possible ways to supply the initial data for a string we're creating. In this case, we aren't supplying any initial data, so the default constructor string::string() is the only possibility.
string::string() is declared in the header file, the line string s; is translated to a call to that function.
I should point out here that the only file that the compiler needs to figure out how to compile the line string s; is the header file, string1.h. The actual implementation of the string class in string1.cc isn't required, because all the compiler cares about when compiling a program using classes is the contract between the class implementer and the user; that is, the header file. The actual implementation in string1.cc that fulfills this contract isn't needed until the program is linked to make an executable; at that point, the linker will complain if it can't find an implementation of any function that we've referred to.
Now that we've disposed of the default constructor, let's take a look at the line in the string interface definition (Figure stringinterface1) that says string(char* p);.6 This is the declaration for another constructor; unlike the default constructor we've examined, this one has an argument, char* p.7
As we saw in Chapter inventor.htm, the combination of the function name and argument types is called the signature of a function. Two functions that have the same name but differ in the type of at least one argument are distinct functions, and the compiler will use the difference(s) in the type(s) of the argument(s) to figure out which function with a given name should be called in any particular case. Of course, this leads to the question of why we would need more than one string constructor; they all make strings, don't they?
Yes, they do, but not from the same "raw material". It's true that every constructor in the string class makes a string, but each constructor has a unique argument list, which determines exactly how the new string will be constructed. The default constructor always makes an empty string (like the C string ""), whereas the constructor string(char* p) takes a C string as an argument and makes a string that has the same value as that argument.
Susan wasn't going to accept this without a struggle.
Susan: I don't get "whereas theSusan wasn't clear on why the C stringstring(char* p)constructor takes a C string and makes astringthat has the same value as the C string does."
Steve: Well, when the compiler looks at the statement
string n("Test");it has to follow some steps to figure it out.
- The compiler knows that you want to create a
stringbecause you've defined a variable callednwith the typestring; that's whatstring nmeans.- Therefore, since
stringis not a native data type, the compiler looks for a function calledstring::string, which would create astring.- However, there can be several functions named
string::string, with different argument lists, because there are several possible ways to get the initial data for thestringyou're creating. In this case, you are supplying data in the form of a literal C string, whose type ischar*; therefore, a constructor with the signaturestring::string(char*)will match.- Since a function with the signature
string::string(char*)has been declared in the header file, the linestring n("Test");is translated to a call to that function.Susan: So
string(char* p)is just there in case you need it for "any given situation"; what situation is this?
Steve: It depends on what kind of data (if any) we're supplying to the constructor. If we supply no data, then the default constructor is used. If we supply a C string (such as a literal C string), then the constructor that takes a
char*is used, because the type of a C string ischar*.
Susan: So
string s;is the default constructor in case you need something that uses uninitialized objects?Steve: Not quite; the default constructor for the
stringclassisstring::string(), which doesn't need any arguments, because it constructs an emptystring.
Susan: And the
string n ("Test");is a constructor that finally gets around to telling us what we are trying to accomplish here?
Steve: Again, not quite. The line
string n("Test");causes astringwith the value "Test" to be created, by calling the constructorstring::string(char* p);.
Susan: See, you are talking first about
string n("Test");in Figure strtst1 and then you get all excited that you just happen to havestring::string(char* p)hanging around which is way over in Figure stringinterface1.
Steve: Now you know that a literal C string such as
"Test"has the data typechar*. Does this make sense?
Susan: OK, I think this helped. I understand it better. Only now that I do, it raises other questions that I accepted before but now don't make sense due to what I do understand. Does that make sense to you? I didn't think so.
Steve: Sure, why not? You've reached a higher level of understanding, so you can now see confusions that were obscured before.
Susan: So this is just the constructor part? What about the default constructor, what happened to it?
Steve: We can't use it here, because we have some data to assign to the
stringwhen thestringis created. A default constructor is used only when there is no initial value for a variable.
Susan: So was the whole point of discussion about default constructors just to let us know that they exist even though you aren't really using them here?
Steve: We are using them to create
strings with no initial value, as discussed before.
"Test" would be of type char*, which is understandable because that's anything but obvious. Here's the discussion we entered into on this point.
Susan: When you sayNow that the reason why a literal C string is of type"Test"is a literal C string of typechar*and that the compiler happily finds that declaration, that is fine. But see, it is not obvious to me that it is typechar*; I can seecharbut notchar*. Something is missing here so that I would be able to follow the jump fromchartochar*.
Steve: A literal C string isn't a single
char, but a bunch ofchars. Therefore, we need to get the address of the first one; that gives us the addresses of the ones after it.
char* is a bit clearer, Figure stringcharstarcons shows the implementation for the constructor that takes a char* argument.
char* constructor for the string class (from code\string1.cc)
(Figure stringcharstarcons)string::string(char* p): m_Length(strlen(p) + 1),
m_Data(new char [m_Length])
{
memcpy(m_Data,p,m_Length);
}
You should be able to decode the header string::string(char* p): This function is a constructor for class string (because its class is string and its name is also string); its argument, named p, is of type char*. The first member initialization expression is m_Length(strlen(p) + 1). This is obviously initializing the string's length (m_Length) to something, but what?
As you may recall, C strings are stored as a series of characters terminated by a null byte (i.e., one with a 0 value). Therefore, unlike the case with our strings, where the length is available by looking at a member variable (m_Length), the only way to find the length of a C string is to search from the beginning of the C string until you get to a null byte. Since this is such a common operation in C, the C standard library provides the function strlen (short for "string length") for this purpose; it returns a result indicating the number of characters in the C string, not including the null byte. So the statement m_Length = strlen(p) + 1; sets our member variable m_Length to the length of the C string p, which we compute as the length reported by strlen (not including the terminating null byte) + 1 for the terminating null byte.8 So the member initialization expression m_Length(strlen(p) + 1), initializes our member variable m_Length to the length of the C string p, which we compute as the length reported by strlen (which doesn't include the terminating null byte) + 1 for the terminating null byte. We need this information because we've decided to store the length explicitly in our string class rather than relying solely on a null byte to mark the end of the string, as is done in C.9 Susan had some questions about the implementation of this function, and I supplied some answers.
Susan: What isstrlen?
Steve: A function left over from C; it tells us how long a C string is.
Susan: Where did it come from?
Steve: It's from the standard C library, which is part of C++.
Susan: What are you using it for here?
Steve: Finding out how long the C string is that we're supposed to copy into our string.
Susan: Is this C or C++?
Steve: Both.
Susan: Why is
char*so special that it deserves a pointer? What makes it different?
Steve: The
*means "pointer". In C++,char*means "pointer to achar".
Susan: I just don't understand the need for the pointer in
char. See when we were using it (char) before, it didn't have a pointer, so why now? Well, I guess it was because I thought it was native back then when I didn't know that there was any other way. So why don't you have a pointer tostrings then? Are all variables inclasses going to have to be pointed to? I guess that is what I am asking.
Steve: We need pointers whenever we want to allocate an amount of memory that isn't known until the program is executing. If we wanted to have a rule that all
strings could be only 10 characters in length (for example), then we could allocate the space for those characters in thestring. However, we want to be able to handlestrings of any length, so we have to decide how much space to allocate for the data when the constructorstring::string(char* p)is called; the only way to do that is to use a pointer to memory that is allocated at run time, namelym_Data. Then we can use that memory to hold a copy of the C string pointed to by the parameterp.
Susan: Oh, no! Here we go again. Is
m_Dataa pointer? I thought it was just a variable that held an address.
Steve: Those are equivalent statements.
Susan: Why does it point? (Do you know how much I am beginning to hate that word?) I think you are going to have to clarify this.
Steve: It "points" in a metaphorical sense, but one that is second nature to programmers in languages like C. In fact, it merely holds the address of some memory location. Is that clearer?
Susan: So the purpose of
m_Datais just a starting off point in memory?
Steve: Right. It's the address of the first
charused to store the value of thestring.
Susan: So the purpose of
m_Lengthis to allot the length of memory that starts at the location wherem_Datais?
Steve: Close; actually, it's to keep track of the amount of memory that has been allocated for the data.
Susan: But I see here that you are setting
m_Lengthtostrlen, so that in effect makesm_Lengthdo the same thing?
Steve: Right;
m_Lengthis the length of thestringbecause it is set to the result returned bystrlen(after adding 1 for the null byte at the end of the C string).
Susan: Why would you want a
stringwith no data, anyway? What purpose does that serve?
Steve: So you can define a
stringbefore knowing what value it will eventually have. For example, the statementstring s;defines a string with no data; the value can be assigned to thestringlater.
Susan: Oh yeah, just as you would have
short x;. I forgot.
Steve: Yep.
Susan: Anyway, the first thing that helped me understand the need for pointers is variable-length data. I am sure that you mentioned this somewhere, but I certainly missed it. So this is a highlight. Once the need for it is understood then the rest falls in place. Well, almost; it is still hard to visualize, but I can.
Steve: I'll make sure to stress that point until it screams for mercy.
Susan: I think you might be able to take this information and draw a schematic for it. That would help. And show the code next to each of the steps involved.
Steve: Don't worry, we'll see lots of diagrams later.
Susan: So
strlenis a function like any member function?
Steve: Yes, except that it is a global function rather than a member function belonging to a particular
class. That's because it's a leftover from C, which doesn't haveclasses.
Susan: So it is what I can consider as a native function? Now I am getting confused again. I thought that just variables can be either made up (
classes) or native. Why are we talking about functions in the same way? But then I remember that, in a backward way, functions belong to variables inclasses rather than the other way around. This is just so crazy.
Steve: Functions are never native in the way that variables are; that is, built into the language. A lot of functions come with the language, in the form of the libraries, but they have no special characteristics that make them "better" than ones you write yourself. However, this is not true of variables; in C, it's impossible to add variable types that have the appearance and behavior of the native types.
Susan: You see I think it is hard for me to imagine a function as one word, because I am so used to
main()with a bunch of code following it and I think of that as the whole function; see where I am getting confused?
Steve: When we call a function like
strlen, that's not the whole function, it's just the name of the function. This is exactly like the situation where we wroteAverageand then called it later to average some numbers.
Susan: A function has to "do something", so you will have to define what the function does; then when we use the function, we just call the name and that sets the action in gear?
Steve: Exactly.
Susan: Now, about this
char*thing. . . (don't go ballistic, please) . . . exactly what is it? I mean it is a pointer tochar, so what is*? Is it like an assignment operator? How is it classified?
Steve:
*means "pointer to the type preceding". Sochar*means "pointer tochar",short*means "pointer toshort", and so on.
Susan: So that would be for a
shortwith variable-length data? And that would be a different kind ofshortthan a nativeshort?
Steve: Almost, but not quite, correct. It would be for variable-length data consisting of one or more
shorts, just as a literal C string is variable-length data consisting of one or morechars.
Susan: So it would be variable by virtue of the number of
shorts?
Steve: Actually, by virtue of the possibility of having a number of
shorts other than exactly one. If you might need two or three (or 100, for that matter)shorts (or any other type), and you don't know how many when the program is compiled, then you pretty much have to use a pointer.
Susan: OK, yes, you said that about
*and what it means to use achar*, but I thought it would work only withcharso I didn't know I would be seeing it again with other variable types. I can't wait.
Steve: Luckily, it appears that we will avoid pointers to types other than
char, due to space constraints. I'll bet you're terribly disappointed.
The next member initialization expression in the constructor, m_Data(new char [m_Length]), is the same as the corresponding expression in the default constructor; in this case, of course, the amount of memory being allocated is equal to the length of the input C string (including its terminating null byte), rather than the fixed value 1.
Now that we have the address of some memory that belongs to us, we can use it to hold the characters that make up the value of our string. The literal value that our test program uses to call this constructor is "Test", which is four characters long, not counting the null byte at the end; since we have to make room for that null byte, the total is 5 bytes, so that's what we'd ask for from new. Assuming that the return value from new was 1234febc, Figure strcons3 illustrates what our new string looks like at this point.
string n during construction
(Figure strcons3)
Address Name string n +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 1234febc +-+ +--------------------+ | | +--------------------+ | 1234febc (none) | ???? |±+ +--------------------+
The reason for the ???? is that we haven't set the data at that location to any value yet, so we don't know what it contains. Actually, this brings up a point we've skipped so far, where new gets the memory it allocates. The answer is that all of the "free" memory in your machine (i.e., memory that isn't used to store the operating system, the code for your program, statically allocated variables, and the stack) is lumped into a large area called the heap. This is where dynamically allocated memory "lives".10 That's a loose way of stating what actually happens, which is that new cordons off part of the heap as being "in use" and returns a pointer to that portion.
It's possible that the idea of a variable that holds a memory address but which is itself stored in memory isn't that obvious. It wasn't to Susan:
Susan: I don't get this stuff about a pointer being stored in a memory address and having a memory address in it. So what's the deal?
Steve: Here's an analogy that might help. Have you ever seen a set of mailboxes for an apartment building? The regular mailboxes are pretty small, but what do they do when you get something that is too big to fit into your regular mailbox? One solution is to put the larger object into one of a few large mailboxes, and leave the key to the larger mailbox in your regular mailbox.
So at this point, we have allocated m_Length bytes of memory, which start at the address in the pointer variable m_Data. Now we need to copy the current value of the input C string (pointed to by p) into that newly allocated area of memory. This is the job of the sole statement inside the constructor proper {}, memcpy(m_Data,p,m_Length); which copies the data from the C string pointed to by p to our newly allocated memory.
The final result is that we have made (constructed) a string variable and set it to a value specified by a C string. To see what our string might look like in memory, see Figure strnmem. But how would this string::string(char* p) constructor operate in a program? To answer that question, Figure strtst1x gives us another look at our sample program.
string class (code\strtst1.cc)
(Figure strtst1x)code/strtst1.cc
How does the compiler interpret the line string n("Test");? First, it determines that string is the name of a class. A function with the name of a class, as we have already seen, is always a constructor for that class. The question is which constructor to call; the answer is determined by the type(s) of the argument(s). In this case, the argument is a literal C string, which has the type char*; therefore, the compiler looks for a constructor for class string that has an argument of type char*. Since there is such a constructor, the one we have just examined, the compiler generates a call to it. Figure strcharstarx shows it again for reference while we analyze it.
char* constructor for the string class, again (from code\string1.cc)
(Figure strcharstarx)string::string(char* p): m_Length(strlen(p) + 1),
m_Data(new char [m_Length])
{
memcpy(m_Data,p,m_Length);
}
When the program executes, string::string(char* p) is called with the argument "Test". Let's trace the execution of the constructor, remembering that member initialization expressions are executed in the order in which the member variables being initialized are listed in the class interface, not necessarily in the order in which they are written in the member initialization list.
m_Length(strlen(p) + 1). This initializes the member variable m_Length to the length of the C string whose address is in p, including the null byte that terminates the string. In this case, the C string is "Test", and its length, including the null byte, is 5.
m_Data(new char [m_Length]) is executed. This allocates m_Length (5, in this case) bytes of memory from the heap and initializes the variable m_Data to the address of that memory.
memcpy(m_Data,p,m_Length); copies m_Length bytes (5, in this case) of data from the C string pointed to by p to the memory pointed to by m_Data.
When the constructor is finished, the string variable n has a length, 5, and contents, "Test", as shown in Figure strnmem. It's now ready for use in the rest of the program. After all the discussion, Susan provided this rendition of the char* constructor for the string class.
Susan: So first we define aclass. This means that we will have to have one or more constructors, which is a function with the same name as theclass, used to create objects of thatclass. Thechar*constructor we're dealing with here goes through three steps, as follows: Step 1 sets the length of thestring; step 2 gets the memory to store the data, and provides the address of that memory; step 3 does the work; it copies what you want.
Steve: Right.
Now, let's look at the next line: s = n;. That looks harmless enough; it just copies one string, n, to another string, s.11 But wait a second; how does the compiler know how to assign a value to a variable of a type we've made up? Just as the compiler will generate a version of the default constructor if we don't define one, because every object has to be initialized somehow, the ability to assign one value of a given type to a variable of the same type is essential to being a data type; therefore, the compiler will supply a version of operator =, the assignment operator, if we don't define one ourselves. In Chapter inventor.htm, we were able to rely on the compiler-generated operator =, which simply copies every member variable from the source object to the target object. That was perfectly fine for our StockItem and Inventory objects, so wouldn't it do the job here? Unfortunately, no. The reason is that the member variable m_Data isn't really the data for the string; it's a pointer to (i.e., the address of) the data. The compiler-generated =, however, doesn't know how we're using m_Data, so it copies the pointer rather than the data. In our example, s = n;, the member variable m_Data in s would end up pointing to the same place in memory as the member variable m_Data in n. Thus, if either s or n did something to change "its" data, both strings would have their values changed, which isn't how we expect variables to behave.
To make this more concrete, let's suppose that our string object n looks like Figure strnmem in memory.
string n in memory
(Figure strnmem)Address Name string n +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 1234febc +-+ +--------------------+ | | +--------+ | 1234febc (none) | "T" |±------------+ +--------+ +--------+ 1234febd (none) | "e" | +--------+ +--------+ 1234febe (none) | "s" | +--------+ +--------+ 1234febf (none) | "t" | +--------+ +--------+ 1234fec0 (none) | 0 | +--------+
So far, so good. We have an object of type string, which contains a length and a pointer to a dynamically allocated memory area where its actual data are stored. However, if we use the compiler-generated operator = to execute the statement s = n;, the result looks like Figure strnsmem1.
strings n and s in memory after compiler-generated =
(Figure strnsmem1)
Address Name string n +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 1234febc +-+ +--------------------+ | | | +--------+ | 1234febc (none) | "T" |±------------+ +--------+ | +--------+ | 1234febd (none) | "e" | | +--------+ | +--------+ | 1234febe (none) | "s" | | +--------+ | +--------+ | 1234febf (none) | "t" | | +--------+ | +--------+ | 1234fec0 (none) | 0 | | +--------+ | | string s | +--------------------+ | 12340020 m_Length | 0005 | | +--------------------+ | 12340022 m_Data | 1234febc +-+ +--------------------+
In other words, the two strings s and n are like Siamese twins; whatever affects one of them affects the other, since they share one copy of the data "Test". What we really want is two strings that are independent of one another, so that we can change the contents of one without affecting the other one. Very shortly, we'll see how to accomplish this.
As you might suspect, Susan didn't think the need for us to define our own operator = was obvious at all. Here's how I started talking her into it.
Susan: I have a little note to you off to the side in the margins about thisoperator =, it says "If it was good enough for native data then why notclassdata?" I think that is a very good question, and I don't care about that pointy thing. I don't understand whym_Dataisn't really data for thestring; it is part of thestring, so why isn't it part of its data?
Steve: It points to the data for the
string, which is a bunch of characters. It isn't the characters itself, but their address.
Susan: Actually, looking at these figures makes this whole idea more understandable. Yes, I see somewhat your meaning in Figure strnsmem1; that pointy thing is pointing all over the place. Oh no, I don't want to see how to make two independent
strings! Just eliminate the pointy thing and it will be all better. OK?
Steve: Sorry, that isn't possible. You'll just have to bear with me until I can explain it to you better.
Susan: Well, let me ask you this: Is the whole point of writing the statement
s=njust to sneak your way into this conversation about this use ofoperator =? Otherwise, I don't see where it would make sense for the sample program.
Steve: Yes, that's correct.
Susan: And the chief reason for creating a new
=is that the new one makes a copy of the data using a new memory address off the heap, rather than having the pointer pointing to the same address while using the compiler-generatedoperator =? If so, why? Getting a little fuzzy around that point. WithStockItem, the compiler-generatedoperator =was good enough. Why not now?
Steve: Yes, that's why we need to create our own
operator =. As for why we didn't need one before: Since the components of aStockItemare all concrete data types, we don't have to worry about "sharing" the data as we do with thestringclass, which contains achar*.
Susan: So when you use
char*or anything with a pointer, that is outside the realm of concrete data types?
Steve: Right. However, the reason that we can't allow pointers to be copied as with the compiler-generated
operator =isn't that they aren't concrete data types, but that they aren't the actual data of thestrings. They're the address of the actual data; therefore, if we copy the pointer in the process of copying a variable, both pointers hold the same address. This means that changes to one of the variables affects the other one, which is not how concrete data types behave.Susan: I think I actually understand this now. At least, I'm not as confused as I was before.
Steve: Good; it's working.
Although it's actually possible to get the effect of two independent strings without the extra work of allocating memory and copying data every time an assignment is done, the mechanisms needed to do that are beyond the scope of this book. By far the easiest way to have the effect of two independent strings is to actually make another copy of a string's data whenever we copy the string, and that's how we'll do it here. The results will be as indicated in Figure strnsmem2.
strings n and s in memory after custom =
(Figure strnsmem2)
Address Name string n +--------------------+ 12340000 m_Length | 0005 | +--------------------+ 12340002 m_Data | 1234febc +-+ +--------------------+ | +--------+ | 1234febc (none) | "T" |±------------+ +--------+ +--------+ 1234febd (none) | "e" | +--------+ +--------+ 1234febe (none) | "s" | +--------+ +--------+ 1234febf (none) | "t" | +--------+ +--------+ 1234fec0 (none) | 0 | +--------+ string s +--------------------+ 12340020 m_Length | 0005 | +--------------------+ 12340022 m_Data | 12345600 +-+ +--------------------+ | | +--------+ | 12345600 (none) | "T" |±------------+ +--------+ +--------+ 12345601 (none) | "e" | +--------+ +--------+ 12345602 (none) | "s" | +--------+ +--------+ 12345603 (none) | "t" | +--------+ +--------+ 12345604 (none) | 0 | +--------+
With this arrangement, a change to one of the string variables will leave the other one unaffected, as the user would expect. To make this happen, we have to implement our own operator =, which will copy the data rather than just the pointer to the data. That's the operator declared in Figure stringinterface1 by the following line:
string& operator = (const string& Str);
What exactly does this mean? Well, as with all function declarations, the first part of the function declaration indicates the return type of the function. In this case, we're going to return a reference to the string to which we're assigning a value; that is, the string on the left of the = sign in an assignment statement. While this may seem reasonable at first glance, actually it's not at all obvious why we should return anything from operator =. After all, if we say a = b;, after a has been set to the same value as b, we're done; that operation is performed by the = operator, so no return value is needed after the assignment is completed.
However, there are two reasons why assignment of native types returns a value, which is equal to the value that was assigned to the left hand argument of =. First, it allows us to write an if statement such as if (a = b), when we really meant if (a == b); of course, this will cause a bug in the program, since these two statements don't have the same meaning. The first one sets a to b and returns the value of a; if a isn't 0, then the if condition is considered true. The latter statement, of course, compares a and b for equality and makes the if condition true if they are equal. To help prevent the error of substituting = for == in this situation, many compilers have a warning that indicates your use of, say, if (a = b); unfortunately, this is a legal construction with native types, and so cannot generate a compiler error. As it happens, using = in this way is an illegal operation with class objects, so even if you want to use this error-prone construction, you can't. Since I never use that construction with native variables, I don't mind not having it for class objects.
The other potential use of the return value from operator = is to allow statements such as a = b = c; where the current value of c is assigned to b and the return value from that assignment is assigned to a. Although I don't use that construction either, since I find it more confusing than useful, I have been told that this ability will be required to use some of the library facilities specified in the forthcoming C++ standard. Therefore, it is my obligation to teach you the "right" way to write assignment operators so that you will be able to use these standard facilities with your classes.
Now we're up to the mysterious looking construct operator =. This portion of the function declaration tells the compiler the name of the function we're defining; namely, operator =. The operator keyword lets the compiler know that the "name" of this function is actually an operator name, rather than a "normal" function name. We have to say operator = rather than merely =, for two reasons. First, because normal function names can't have a = character in them, but are limited to upper and lower case letters, numbers, and the underscore (_). Second, because when we're redefining any operator, even one (like new) whose name is made of characters allowed in identifiers, we have to tell the compiler that we're doing that on purpose. Otherwise, we'll get an error telling us that we're trying to define a function or variable with the same name as a keyword.12 We're ready to look at the argument to this function, specified by the text inside the parentheses, const string& Str. We've already seen in Chapter inventor.htm that & in this context means that the argument to which it refers is a reference argument rather than a value argument.13 In other words, the variable Str is actually just another name for the argument provided by the caller of this function, rather than being a separate local variable with the same value as the caller's argument. However, there is a new keyword in this expression: const, which is short for "constant". In this context, it means that we promise that this function will not modify the argument to which const refers, namely string& Str. This is essential in the current situation, but it will take some discussion to explain why.
As you may recall from Chapter function.htm, when you call a function using a value argument, the argument that you give in the calling function isn't the one that the called function receives. Instead, a copy is made of the calling function's argument, and the called function works on the copy. While this is fine most of the time, in this case it won't work properly, for reasons that will be apparent shortly; instead, we have to use a reference argument. As we saw in the discussion of reference arguments in Chapter inventor.htm, such an argument is not a copy of the caller's argument, but another name for the actual argument provided by the caller. This has a number of consequences. First, it's more efficient than a "normal" argument, because the usual processing time needed to make a copy for the called function isn't required. Second, any changes made to the reference argument change the caller's argument as well. The use of this mechanism should be limited to those cases where it is really necessary, since it can confuse the readers of the calling function. There's no way to tell just by looking at the calling function that some of its variables can be changed by calling another function.
In this case, however, we have no intention of changing the input argument. All we want to do is to copy its length and data into the output string, the one for which operator = was called. Therefore, we tell the compiler, by using the const modifier, that we aren't going to change the input argument. This removes the drawback of non-const reference arguments: that they can change variables in the calling function with no indication of that possibility in the calling function. Therefore, using const reference arguments is quite a useful and safe way to reduce the number of time-consuming copying operations needed to make function calls.
The use of a const reference argument in this case is more than just efficient, however; as we'll see in the discussion in Chapter downpath.htm under the heading "Temporary Help Wanted", such an argument allows us to assign a C string (i.e., bytes pointed to by a char*) to one of our string variables without having to write a special operator = for that purpose.
You might be surprised to hear that Susan didn't have too much trouble accepting all this stuff about const reference arguments. Obviously her resistance to new ideas was weakening by this point.
Susan: OK, so the reference operator just renames the argument and doesn't make a copy of it; that is why it is important to promise not to change it?Back to the discussion of the function declaration, we now have enough information to decode the function declaration
Steve: Right. A non-
constreference argument can be changed in the function, because unlike a "regular" (i.e., value) argument, which is really a copy of the calling function's variable, a reference argument is just another name for the caller's variable. Therefore, if we change the reference argument, we're really changing the caller's variable, which is generally not a good idea.
Susan: OK. But in this case since we are going to want to change the meaning of
=in allstrings it is OK?
Steve: Not quite. Every time we define an operator we're changing the meaning of that operator for all objects of that
class. The question is whether we're intending to change the value of the caller's variable that is referred to by the reference argument. If we are, then we can't useconstto qualify the reference; if not, we can useconst. Does that answer your question?
Susan: Well, yes and no. I think I have it now: When you write that code it is for that
classonly and won't affect otherclasses that you may have written, because it is contained within that particularclasscode. Right?
Steve: Correct.
Susan: So we don't want to change the input argument because what we are doing is basically defining a new
=for thisclass, right?
Steve: Right. The input argument is where we get the data to copy to the
stringwe're assigning to. We don't want to change the input argument, just thestringwe're assigning to.
string& string::operator = (const string& Str)
as illustrated in Figure operatoreqstring.
operator = for the string class
(Figure operatoreqstring)This function returns a reference to a string; | it belongs to the stringclass; | | it implements operator =; | | | | | and it won't change its argument, | | | which is a reference to | | | | the caller's string; | | | | i.e., another name for | | | | the caller's variable. | | | | | | | | | | +-+-----++---+----++---+------+ +--+--++--+--------+ string& string:: operator = ( const string& Str )
Putting it all together, we're defining a function belonging to class string that returns a reference to a string. This function implements operator = and takes an argument named Str that's a constant reference to a string. That is, the argument Str is another name for the string passed to us by the caller, not a copy of the caller's string. Furthermore, we're vowing not to use this argument to change the caller's variable.
operator?Now that we've dissected the header into its atomic components, the actual implementation of the function should be trivial by comparison. But first there's a loose end to be tied up. That is, why was this function named string::operator = called in the first place? The line that caused the call was very simple: s = n;. There's no explicit mention of string or operator.
This is another of the ways in which C++ supports classes. Because you can use the = operator to assign one variable of a native type to another variable of the same type, C++ provides the same syntax for user defined variable types. Similar reasoning applies to operators like >, <, and so on, for classes where these operators make sense.
When the compiler sees the statement s = n;, it proceeds as follows:
s is an object of class string.
operator =) setting s equal to the value of another string value named n.
class string that implements operator = and takes one argument of class string?
s = n; into a call to operator = for class string.
Susan was appreciative of the reminder that we started out discussing the statement s = n;.
Susan: Oh, my gosh, I totally forgot abouts = n; thanks for the reminder. We did digress a bit, didn't we? Are you saying you have to go through the same thing to define other operators inclasses?
Steve: Yes.
Susan: So are you saying that when you write the simple statement
s = n;that the=calls the function that we just went through?
Steve: Right.
Following this procedure, the correspondence between the tokens in the original program and the call to the member function should be fairly obvious, as we see them in Figure callingopeq.14
operator = implementation
(Figure callingopeq)
s = n; | | | +-----------------------+ +----------+ | +----+-----+ +------+----------+ string& string:: operator = ( const string& Str )
But we've left out something. What does the string s correspond to in the function call to operator =?
this?The string s corresponds to a hidden argument whose name is the keyword this. Such an argument is automatically included in every call to a member function in C++.15 Its type is always a constant pointer to an object of the class that a member function belongs to. In this case its type is const string*; that is, a constant pointer to a string; the const means that we can't change the value of this by assigning a new value to it. The value of this is the address of the class object for which the member function call was made. In this case, the statement s = n; was translated into s.operator = (n); by the compiler; therefore, when the statement s = n; is being executed, the value of this is the address of the string s.
To see why we need to be concerned about this, let's start analyzing the implementation of operator =. Figure stringassignment shows the code for that function.
operator =) for the string class (from code\string1.cc)
(Figure stringassignment)string& string::operator = (const string& Str){
if (&Str != this)
{
delete [] m_Data;
m_Length = Str.m_Length;
m_Data = new char [m_Length];
memcpy(m_Data,Str.m_Data,m_Length);
}
return *this;
}
In the case of the string class, as in most cases, the only reasons why we have to worry about this are to be able to determine whether two objects that are referred to by different names are actually the same object and to return a reference to the left-hand object in an assignment statement. You can see both uses of this in the code for string::operator = (const string& Str). The first use is illustrated by the following line:
if (&Str != this)
Most of this statement should be familiar by now. Like other if statements, it tests a condition for truth or falsity. The != is the comparison operator that tests for "not equal", and of course we've just seen that this is the address of the variable for which the operator = function was called; in the expression s = n, this would be the address of the variable s. However, that still leaves the expression &Str.
Unfortunately, & is one of the tokens that is used in several different ways depending on context; when it precedes the name of a variable without itself being preceded by a data type like string, it means "the address of the following variable". In this case, &Str means "the address of Str". The variable Str is the argument passed to us by the caller; remember, as a reference argument, Str is another name for the caller's variable, rather than a copy of it. Therefore, &Str is the address of the caller's variable (n, in our example).
Clearly, then, the expression if (&Str != this) is comparing the address of the caller's string (i.e., &Str) to this. As we just saw, this represents the address of the object that we're operating on; in the case of operator =, it's the address of (i.e., a pointer to) the string that is going to be changed by operator =. Therefore, this if statement is checking whether the string that was passed to us by the caller is a different string from the one to which the assignment is supposed to be made. For example, if the source line that caused operator = to be called was a = a; then the if statement would be false and therefore the block controlled by the if wouldn't be executed. Of course, there's no reason to do anything other than return the value of a in that case. However, there are good reasons besides efficiency to check for the attempt to assign a string to itself; those reasons are the subject of one of the exercises in this chapter. For now, let's continue with the contents of the block controlled by the if, assuming that the two strings are actually distinct.
The first statement in the controlled block of the if statement is delete [] m_Data;. This corresponds to the new statement that we used to allocate memory for a string in the constructor string::string(char* p) (Figure stringcharstarcons).16 That is, the delete operator returns the memory to the available pool called the heap. There are actually two versions of the delete operator: One version frees memory for a single data item, and the other frees memory for a group of items that are stored consecutively in memory. Here, we're using the version of the delete operator that frees a group of items rather than a single item, which we indicate by means of the [] after the keyword delete; the version of delete that frees only one item doesn't have the [].17 So after this statement is executed, the memory that was allocated in the constructor to hold the characters in our string has been handed back to the memory allocation routines for possible reuse at a later time.
Susan had a few minor questions about this topic, but nothing too alarming.
Susan: Sodeletejust takes out the memorynewallocated form_Data?
Steve: Right.
Susan: What do you mean by "frees a group of items"?
Steve: It returns the memory to the heap, so it can be used for some other purpose.
Susan: Is that all the addresses in memory that contain the length of the
string?
Steve: Not the length of the
string, but the data for thestring, such as"Test".
delete Me, Let Me GoA point that we should not overlook is the possibility of calling delete for a pointer that has never been assigned a value. Calling delete on a pointer that doesn't point to a valid block of memory allocated by new will cause the system to malfunction in some bizarre way, usually at a time considerably after the improper call to delete.18 This occurs because the dynamic memory allocation system will try to reclaim the "allocated" memory pointed to by the invalid pointer by adding it back to the heap. Eventually, some other function will come along, ask for some memory, and be handed a pointer to this "available" block that is actually nothing of the sort. The result of trying to use this area of memory depends on which of three cases the erroneous address falls into: The first is that the memory at that address is nonexistent, the second is that the memory is already in use for some other purpose, and the third is that the invalid address points to data that is already in the heap. In the first case, the function that tries to store its data in this nonexistent area of memory will cause a system crash or error message, depending on the system's ability and willingness to check for such errors; the "General Protection Fault" message so familiar to Windows users is caused by this sort of error. In the second case, the function that is the "legal" owner of the memory will find its stored values changed mysteriously and will misbehave as a result. In the third case, the heap management routines would probably get confused and start handing out wrong addresses. Errors of this kind are common in programs that use pointers heavily and are extremely difficult to find.
Susan was interested in this topic of errors in memory allocation, so we discussed it.
Susan: Can you give me an example of what an "invalid pointer" would be? Would it be an address in memory that is in use for something else rather than something that can be returned to the heap?
Steve: That's one kind of invalid pointer. The other type would be an address that doesn't exist at all; that is, one that is past the end of the possible legal addresses.
Susan: Oh, wait, so it would be returned to the heap but later if it is allocated to something else, it will cause just a tiny little problem because it is actually in use somewhere else?
Steve: You bet.
Susan: Is this a crash?
Steve: This is a major cause of crashes.
Susan: Oh yeah, this is cool, this is exciting. So this is what really happens when a crash occurs?
Steve: Do you know what a GPF (General Protection Fault) is? It's the operating system detecting the attempt by a program to access memory that doesn't belong to it.
Susan: I like this. So when there is no memory address allocated you get a error message?
Steve: Yes, or if it belongs to someone else. Of course, you'll be lucky to get anything other than a hard crash if it's a DOS program; at least in Windows you'll probably get a GPF instead.
Another way to go wrong with dynamic memory allocation is the opposite one. Instead of trying to delete something that was never dynamically allocated, you can forget to delete something that has been dynamically allocated. This is called a memory leak; it's very insidious, because the program appears to work correctly when tested casually. The usual way to find these errors is to notice that the program runs apparently correctly for a (possibly long) time and then fails due to running out of available memory. I should mention here how we can tell that we've run out of memory: The new operator returns a pointer with a 0 value. By convention, a pointer with the value 0 means "a pointer that doesn't point to anything". Hence, 0 is a reasonable value to return when there is no memory left. Our sample programs don't check the return value from new, so they will go along merrily as though they have actually allocated memory successfully. The result will be a spectacular failure when we actually try to use the "allocated" memory at location 0. Of course, a real program should check for this problem and tell the user that no more memory is left (if no better solution is available); , however, plenty of programs that have supposedly been working for a long time fail to check this situation sometimes.19 Given all of the ways to misuse dynamic memory allocation, we'll use it only when its benefits clearly outweigh the risks. To be exact, we'll restrict its use to controlled circumstances inside class implementations, to reduce the probability of such errors.
Susan had some questions about the idea of new returning a 0 if no memory is left.
Susan: Where is it written thatnewreturns a value of 0?
Steve: That's what
newdoes when it doesn't have anything to give you. By convention, 0 isn't a valid pointer value; therefore, getting a 0 fromnewmeans you're hosed.
Susan: How does a real program check for this?
Steve: By saying something like this:
p = new char[1000];
if (p == 0)
{
cout << "You're hosed!" << endl;
exit(1);
}
For your information,
exitmeans "bail out of the program right now, without returning to the calling function, if any". The argument toexitis reported back to DOS as the return value from the program; 0 means OK, anything else means some sort of error.
The error prone nature of dynamic memory allocation is ironic, since it would be entirely possible for the library implementers (that is, the people who write the functions that are used by new and delete) to prevent or at least detect the problem of deleting something you haven't allocated or failing to delete something that you have allocated. After all, those routines handle all of the memory allocation and deallocation for a C++ program, so there's no reason that they couldn't keep track of what has been allocated and not released.
Of course, an ounce of prevention is worth a pound of cure, so avoiding these problems by proper design is the best solution. Luckily, it is possible to write programs so that this type of error is much less likely; basically, this approach requires keeping all dynamic memory allocation inside class implementations, rather than exposing it to the application programmer. We're following this approach with our string class, and it can also be applied to other situations where it is less straightforward. However, such applications are beyond the scope of this book.
Susan was intrigued by the possible results of forgetting to deallocate resources such as memory. Here's what resulted from her interest:
Susan: So when programs leak system resources, is that the result of just forgetting to delete something that is dynamically allocated?
Steve: Yes.
Susan: Then that would be basically a programming error or at least sloppiness on the part of the programmer?
Steve: Yes.
Having discussed some of the possible problems with dynamic allocation, let's continue with the code for operator = (Figure stringassignment). The next statement is m_Length = Str.m_Length;. This is the first time we've used the . operator to access a member variable of an object other than the object for which the member function was called. Up until now, we've been satisfied to refer to a member variable such as m_Length just by that simple name, as we would with a local or global variable. The name m_Length is called an unqualified name because it doesn't specify which object we're referring to. The expression m_Length by itself refers to the occurrence of the member variable m_Length in the object for which the current function was called; i.e., the string whose address is this (the string s in our example line s = n;). If you think about it, this is a good default, because member functions refer to member variables of their "own" object more than any other kinds of variables. Therefore, to reduce the amount of typing the programmer has to do, whenever we refer to a member variable without specifying the object to which it belongs, the compiler will assume that we mean the variable that belongs to the object for which the member variable was called (i.e, the one whose address is the current value of this). However, when we want to refer to a member variable of an object other than the one pointed to by this, we have to indicate which object we're referring to, which we do by using the . operator. This operator means that we want to access the member variable (or function) whose name is on the right of the . for the object whose name is on the left of the .. Hence, the expression Str.m_Length specifies that we're talking about the occurrence of m_Length that's in the variable Str, and the whole statement m_Length = Str.m_Length; means that we want to set the length of "our" string (i.e., the one pointed to by this) to the length of the argument string Str.
Susan had some questions about this issue of accessing variables of another string and how that relates to operator =.
Susan: I still don't get theNext, we use the statement.thingy.
Steve: All
.does is separate the object (on the left) from the member variable or function (on the right). Sos.operator=(n);might be roughly translated as "apply the operator=to the objects, with the argumentn".
Susan: So wait: the
.does more than separate; it allows access to other string member variables?
Steve: It separates an object's name from the particular variable or function that we're accessing for that object. In other words,
Str.m_Lengthmeans "the instance ofm_Lengththat is part of the objectStr."
Susan: So in the statement
m_Length = Str.m_Length;what we are doing is creating a newm_Lengthequal to the length ofStr'sm_Lengthfor the=operator?
Steve: What we're doing is setting the value of the length (
m_Length) for thestringbeing assigned to (the left-handstring) to the same value as the length of thestringbeing copied from (the right-handstring).
Susan: But it is going to be specific for this string?
Steve: If I understand your question, the value of
m_Lengthwill be set for the particularstringthat we're assigning a new value to.
Susan: When we say
Str, does that mean that we are not using the variable pointed to bythis? I am now officially lost.
Steve: Yes, that's what it means. In a member function, if we don't specify the variable we are talking about, it's the one pointed to by
this; of course, if we do specify which variable we mean, then we get the one we specify.
m_Data = new char [m_Length]; to acquire the address of some memory that we will use to store our new copy of the data from Str; along with the address, new gives us the right to use that memory until we free it with delete.
Then we use memcpy to copy the data from Str (i.e., the group of characters starting at the address stored in Str.m_Data) to our newly allocated memory, which of course is pointed to by m_Data (i.e., the occurrence of m_Data in the string being assigned to). Now our target string is a fully independent entity with the same value as the string that was passed in.
Finally, we return *this, which means "the object to which this points", i.e., a reference to the string whose value we have just set, so that it can be used in further operations.
Although the individual statements weren't too much of a problem, Susan didn't get the big picture. Here's how I explained it to her:
Susan: I don't get this whole code thing for Figure stringassignment, now that I think about it. Why does this stuff make a newoperator =? This is weird.
Steve: Well, what does
operator =do? It makes the object on the left side have the same value as the object on the right side. In the case of astring, this means that the left-handstringshould have the same length as the one on the right, and all thechars used to store the data for the right-handstringneed to be copied to the address pointed to bym_Datafor the left-handstring. That's what our custom=does.
Susan: Let's see. First we have to get some new memory for the new
m_Data; then we have to make a copy. . . So then the entire purpose of writing a newoperator =is to make sure that variables of thatclasscan be made into separate entities when using the=sign rather than sharing the same memory address for their data?
Steve: Right.
Susan: I forget now why we did that.
Steve: We did it so that we could change the value of one of the variables without affecting the other one.
Before we move on to the next member function, I should mention that Susan and I had quite a lengthy correspondence about the notion of this. Here are the highlights of that discussion.
Susan: I don't understandFigure stringassignmentnothis shows what the code forthis.
Steve:
thisrefers to the object that a member function is being called for. For example, in the statementxyz.Read();, when the function namedReadis called, the value ofthiswill be the address of the objectxyz.
Susan: OK, then, is
thisthe result of calling a function? Or the address of the result?
Steve: Not quite either of those;
thisis the address of the object for which aclassfunction is called.
Susan: Now that I have really paid attention to
thisand tried to commit it to memory it makes more sense. I think that what is so mysterious is that it is a hidden argument. When I think of an argument I think of something in (), as an input argument.
Steve: It actually is being passed as though it were specified in every call to a member function. The reason it is hidden is not to make it mysterious, but to reduce the amount of work the programmer has to do. Since almost every member function needs to access something via
this, supplying it automatically is a serious convenience.
Susan: Now as far as my understanding of the meaning of
this, it is the address of the object whose value is the result of calling a member function.
Steve: Almost exactly right;
thisis the address of the object for which a member function is called. Is this merely a semantic difference?
Susan: Not quite. Is there not a value to the object? Other than that we are speaking the same language. We usually do.
Steve: Yes, the object has a value. However,
thisis merely the address of the object, not its value.
Susan: How about writing
thisas if it were not hidden and was in the argument list; then show me how it would look. See what I mean? Show me what you think it would look like if you were to write it out and not hide it.
Steve: OK, that sounds good. Actually, I was thinking of doing that anyway.
operator = might look like if the this pointer weren't supplied automatically, both in the function declaration and as a qualifier for the member variable names.20
operator =) for the string class with explicit this
(Figure stringassignmentnothis)string& string::operator = (const string* this, const string& Str){
if (&Str != this)
{
delete [] this->m_Data;
this->m_Length = Str.m_Length;
this->m_Data = new char [this->m_Length];
memcpy(this->m_Data,Str.m_Data,this->m_Length);
}
return *this;
}
Note that every reference to a member variable of the current object would have to specify this. That would actually be more significant in writing the code than the fact that we would have to supply this in the call. Of course, how we would actually supply this when calling the operator = function is also a good question. Clearly the necessity of passing this explicitly would make for a messier syntax than just s = n;.
Now that we have seen how operator = works in detail, let's look at the next member function in the initial version of our string class, the destructor.21 A destructor is the opposite of a constructor; that is, it is responsible for deallocating any memory allocated by the constructor and performing whatever other functions have to be done before a variable dies. It's quite rare to call the destructor for a variable explicitly; as a rule, the destructor is called automatically when the variable goes out of scope. As we've seen, the most common way for this to happen is that a function returns to its calling function; at that time, destructors are called for all local variables that have destructors, whereas local variables that don't have destructors, such as those of native types, just disappear silently.22 Susan had some questions about how variables are allocated and deallocated. Here's the discussion that ensued.
Susan: I remember we talked about the stack pointer and how it refers to addresses in memory but I don't remember deallocating anything. What is that?Because destructors are almost always called automatically when a variable goes out of scope, rather than by an explicit statement written by the programmer, the only information guaranteed to be available to a destructor is the address of the variable to be destroyed. For this reason, the C++ language specifies that a destructor cannot have arguments. This in turn means that there can be only one destructor for any
Steve: Deallocating variables on the stack merely means that the same memory locations can be reused for different local variables. Remember the function
mess?
Susan: Oh, that is right, the data stays in the memory locations until the location is used by something else. It really isn't meaningful after it has been used unless it is initialized, is that right?
Steve: Yes, that's right.
Susan: When I first read about the destructor my reaction was, "well, what is the difference between this and
delete?" But basically it just is a function that makesdeletego into auto-pilot?
Steve: Basically correct.
Susan: How does it know you are done with the variable, so that it can put the memory back?
Steve: By definition, when the destructor is called, the variable is history. This happens automatically when it goes out of scope. For an
autovariable, whether of native type orclasstype, this occurs at the end of the function where the variable was defined.Susan: I don't understand this. I reread your explanation of "going out of scope" and it is unclear to me what is happening and what the alternatives are. How does a scope "disappear"?
Steve: The scope doesn't disappear, but the execution of the program leaves it. For example, when a function terminates, the local variables (which have local scope), go out of scope and disappear. That is, they no longer have memory locations assigned to them, until and unless the function starts execution again.
Susan: What if you need the variable again?
Steve: Then don't let it go out of scope.
class, since there can be at most one function in a given class with a given name and the same type(s) of argument(s) (or, as in this case, no arguments).
As with the constructor(s), the destructor has a special name to identify it to the compiler. In this case, it's the name of the class with the token ~ (the tilde) prefixed to it, so the destructor for class string is named ~string.23 The declaration of this function is the next line in Figure stringinterface1, ~string();. Its implementation looks like Figure stringdestr.
string class (from code/string1.cc)
(Figure stringdestr)string::~string(){
delete [] m_Data;
}
This function doesn't use any new constructs; we've already seen that the delete [] operator frees the memory allocated to the pointer variable it operates on.24 In this case, that variable is m_Data, which holds the address of the first one of the group of characters that make up the actual data contained by the string.
Now that we've covered nearly all of the member functions in the initial version of the string class, it's time for some review.
We've almost finished building a concrete data type called the string class, which provides a means of storing and processing a group of characters similar to the facilities provided by a C string, but without some of the drawbacks of the latter data type. The fact that string is a concrete data type means that a string that is defined as a local variable in a function should be created when the function starts up and automatically deleted when the function ends. Also, we need to be able to copy a string to another string and have the two copies behave like independent variables, not linked together in the manner of Siamese twins.
The creation of an object is performed by a special member function called a constructor. Any class can have several constructors, one for each possible way that a newly created object can be initialized. So far, we've examined the interface and implementation of the default constructor, which takes no arguments, and a constructor that takes a char* argument. The former is needed to create a string that doesn't have an initial value, while the latter allows us to create a string that has the same contents as a C string. The default constructor is one of the required member functions in a concrete data type.
We've also seen that in the case of our string constructors, we need to know the order in which the member initialization expressions are executed; since this is dependent on the order of declaration of member variables, we have to make sure that those member variables are declared in the correct order for our member initialization expressions to work properly.
Continuing with the requirements for a concrete data type, we've implemented our own version of operator =, which can set one string to the same value as another string while leaving them independent of one another.
We've also created one other required member function for a concrete data type, the destructor, which is used to clean up after a string when it expires. This member function is called automatically for an auto variable at the end of the function where that variable is defined.
We're still short a copy constructor, which can create a string that has the same value as another pre-existing string. This may sound just like operator =, but it's not exactly the same. While operator = is used to set a string that already exists to the same value as another extant string, the copy constructor creates a brand-new string with the same value as one that already exists. We'll see how this works in the next chapter; in the meantime, let's take a look at some exercises intended to test your understanding of this material.
1. What would happen if we compiled the program in Figure strex1? Why?
code/strex1.cc
2. What would happen if we compiled the program in Figure strex2? Why?
code/strex2.cc
3. What would happen if we compiled the program in Figure strex3? Why?
code/strex3.cc
4. What would happen if a user of our string class wrote an expression that tried to set a string variable to itself (e.g., a = a;) and we hadn't bothered to check for that situation in our operator = (Figure stringassignment).?
5. What would happen if we compiled the program in Figure strex5? Why?
code/strex5.cc
We've covered a lot of material about how a real, generally useful class such as string works in this chapter. In the next chapter, we'll continue with the saga of the string class, finishing up the additional functionality needed to turn it into a full-fledged concrete data type. We'll put this new functionality to the test in a modified version of the sorting algorithm from the early chapters that sorts strings rather than numeric values.
1. The output of the compiler should look something like this:
strex1.cc: In function `int main()':
strex1.cc:21: member `m_Length' is a private member of
class `string'
This one is simple; since m_Length is a private member variable of string, a nonmember function such as main can't access it.
2. The output of the compiler should look something like this:
strex2.cc: In function `int main()':
strex2.cc:9: constructor `string::string()' is private
strex2.cc:17: within this context
strex2.cc:17: in base initialization for
class `string'
This is also pretty simple. Since the default constructor string::string() is in the private area, it's impossible for a nonmember function such as main to use it. Notice that there was no error message about string::string(char* p); that constructor is in the public area, so main is permitted to create a string from a C string. It's just the default constructor that's inaccessible.
3. The output of the compiler should look something like this:
strex3.cc:12: warning: `25class string' only defines a private destructor and has no friendsstrex3.cc: In function `int main()':
strex3.cc:16: destructor for type `string' is private in this scope
This answer is considerably less obvious than the previous ones. To be sure, the destructor is private and can't be called from main, but that doesn't explain why main is trying to call the destructor in the first place. The reason is that every auto variable of a type that has a destructor must have its destructor called at the end of that function. That's part of the mechanism that makes our objects act like "normal" variables, which also lose their values at the end of the function where they are declared.26 In the case of a user defined variable, though, more cleanup may be required; this is certainly true for strings, which have to deallocate the memory that they allocated to store their character data.
Therefore, you cannot create an object of a class whose destructor is private as an auto variable, as the automatic call of the destructor at the end of the scope would be illegal.
Susan didn't get this one exactly right, but she was obviously in the ballpark.
Susan: I have a note here that this program would not work because the~string ()thingy should bepublicand that, if this were to run, it would cause a memory leak. Am I on the right track?
Steve: Yes, you're close. Actually, what would happen is that the compiler would refuse to compile this program because it wouldn't be able to call
~stringat the end of the function, since~stringisprivate. If the compiler were willing to compile this program without calling the destructor, there would indeed be a memory leak.
4. Let's take a look at the sequence of events that would have transpired if the user had typed a = a; and we hadn't taken the precaution of checking for that situation in the operator = code.27The first statement to be executed would be delete [] m_Data;. This gives the memory that had been allocated to store characters in string a back to the operating system.
The second statement to be executed would be m_Length = Str.m_Length;. Since m_Length and Str.m_Length are actually the same memory location in this case, this statement wouldn't do anything.
The third statement to be executed would be m_Data = new char [m_Length];. This would allocate memory for the target string, and assign it to the member variable m_Data.
The fourth statement to be executed would be memcpy(m_Data,Str.m_Data,m_Length);. This would copy m_Length bytes of data to the address stored in m_Data, which points to the newly allocated piece of memory, from the address stored in Str.m_Data, which points to. . . the same address. Remember, if this and &Str are the same, as they are in this case, then m_Data and Str.m_Data are two names for the same memory location. Therefore, this operation will have no effect. Furthermore, the preceding step has assigned the address of the newly allocated memory to m_Data, overwriting the previous contents of m_Data, that is, the address of the original contents of the string a. Therefore, the original value of a, which was pointed to by m_Data when we started, is no longer accessible. Even if we had a copy of that address, we couldn't use it because the memory to which it refers to has already been returned to the operating system and no longer belongs to us.
The net result of all of this is that the m_Data member variable of string a would point to uninitialized data.For a further explanation of this, you might want to take a look at the discussion I had with Susan. The topic is the if statement if (&Str != this), which checks whether the source string is the same as the destination string:
Susan: I don't get the purpose of thisifstatement. Do we want the twostrings to be the same or to be different?
Steve: We want them to be the same after we do the copy; if they're the same before the copy, we don't need (or want) to do anything. This may not be completely obvious, so here's a more detailed explanation:
If the two
strings are different when theifstatement is executed, then we can safely execute the code controlled by theifstatement, because executingdeleteon the old contents of thestringon the left-hand side of the=sign won't affect thestringon the right-hand side.If the two
strings are actually the samestring, then thedeletecall will deallocate the memory that was used to hold thestring's data; therefore, thememcpywill be copying from a pointer that is no longer valid. Even if this code would work, there's no reason to execute it if the twostrings are already the same at the beginning of the code for the assignment statement.
Susan: OK, so if the two
strings are the same then theifstatement won't go into effect, but if they are different, then it will; it is important that they are different otherwise you would be deleting the original address of the caller?
Steve: Right, with one very small modification: It's the data at the original address of the
stringthat would get freed prematurely.
5. This one was a little tricky. I'll bet you thought that making the default constructor private would keep this from compiling, but it turns out that we're not using the default constructor. That should be obvious in the line string n("Test");, which clearly uses string::string(char* p), but what does the compiler do with the line string x = n;? You might think that it calls the default constructor to make x and then uses operator = to copy the value of n into it. If that were true, the private status of the default constructor would prevent the program from compiling. However, what actually happens is that the copy constructor string::string(const string&) is used to make a brand new string called x with the same value as n. So, in this case, the private access specifier on the default constructor doesn't get in the way.
char* can also be written as char *, but I find it clearer to attach the * to the data type being pointed to.
char* the differences between a pointer and a memory address will never matter to us. Since we aren't going to be using any other types of pointers in this book, we can avoid adding yet another source of complexity to this discussion.
char. For example, a pointer to a short would have the type short*, and similarly for pointers to any other data type. However, we won't need to use pointers to any other type of variables in this book.
m_Length, m_Data, and the data for the null C string is to indicate how long those data items are.
p for a pointer. You could call it George if you wanted to, but it would just confuse people. The letter p is often used for pointers, especially by programmers who can't type, which unfortunately is fairly common.
strlen and memcpy come with the language; that is, they are supplied in the standard libraries that you get when you buy the compiler. However, these functions are not privileged relative to the functions you can write yourself, unlike the case with native variables in C. In other words, you can write a function in C or C++ that looks and behaves exactly like one in the library, whereas it's impossible in C to add a type of variable that has the same appearance and behavior as the native types; the knowledge of the native variable types is built into the C compiler and cannot be changed by the programmer.
But why aren't there any native functions? Because the language was designed to be easy to move (or port) from one machine to another. This is easier if the compiler is simpler; hence, most of the functionality of the language is provided by functions that can be written in the "base language" the compiler knows about. This includes basic functions such as strlen and memcpy, which can be written in C. For purposes of performance, they are often written in assembly language instead, but that's not necessary to get the language running on a new machine.
gdb debugger to look at our strings, we have to include that null byte at the end. Otherwise, gdb won't know how to display them in an intelligible format.
operator = works.
operator; we're limited to those operators that already exist in the C++ language.
string is a token, as are :: and (. On the other hand, x = 5; is a statement.
this pointer passed to it, but we won't get to discuss that other type.
char* constructor because we've already analyzed that one.
[] after delete tells the compiler that the latter is the current situation. The C++ standard specifies that any memory that was allocated via a new expression containing [] must be deleted via delete [].
delete for a pointer with the value 0 will not cause any untoward effects, as such a pointer is recognized as "pointing to nowhere".
new when no more memory is left. Instead of returning 0, new will notify the application programmer via a mechanism called an exception. Unfortunately, this mechanism is outside the scope of this book.
->, which is the "object pointer member access" operator. It separates a pointer to an object name, on its left, from the member variable or member function on its right. In other words, -> does the same thing for pointer variables that . does for objects. That is, if the token on the right of -> is a member variable, that token refers to the specific member variable belonging to the object pointed to by the pointer on the left of ->; if the token on the right of -> is a member function, then it is called for the object pointed to by the pointer on the left of ->. For example, this->m_Data means "the m_Data that belongs to the object pointed to by this".
new to allocate memory for a variable that has a destructor, then the destructor is called when that variable is freed by delete. However, we won't be using new to create class objects in this book.
x has the logical value true, then ~x will have the logical value false, and vice-versa.
m_Data and m_Length member variables, we don't have to worry about those because the string being destroyed won't ever be used again.
friend is, it's a class or a function that has special access to the internal workings of another class. We'll get into that mechanism in Chapter downpath.htm.